package com.alogic.bearer;

import com.alogic.json.JsonFactory;
import com.alogic.xscript.Logiclet;
import com.alogic.xscript.LogicletContext;
import com.alogic.xscript.Script;
import com.alogic.xscript.doc.XsObject;
import com.alogic.xscript.doc.json.JsonObject;
import com.anysoft.util.*;
import com.anysoft.util.resource.ResourceFactory;
import com.anysoft.webloader.HttpClientTool;
import com.anysoft.webloader.ServletConfigProperties;
import com.anysoft.webloader.ServletHandler;
import com.logicbus.backend.Context;
import com.logicbus.backend.bizlog.BizLog;
import com.logicbus.backend.server.http.HttpContext;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Bearer Handler
 *
 * @since 1.6.14.13 [2021708 duanyy]
 *
 * @version 1.6.15.2 [20211103 duanyy] <br>
 * - 在doAuth时注入service变量; <br>
 */
public class BearerHandler implements ServletHandler, XMLConfigurable, Configurable,BearerConstants {
    /**
     * a logger of slf4j
     */
    protected final static Logger LOG = LoggerFactory.getLogger(BearerHandler.class);

    /**
     * 缺省配置文件
     */
    protected static final String DEFAULT = "java:///com/alogic/bearer/default.xml#App";

    /**
     * command的前缀
     */
    protected String cmdPrefix = "/auth";

    /**
     * 获取token的地址
     */
    protected String realm = "/auth/token";

    protected String realmMode = "link";

    protected String service = "self";

    /**
     * 编码
     */
    protected String encoding = "utf-8";

    /**
     * 验证当前token及权限
     */
    protected Logiclet onAuth = null;

    /**
     * 获取token
     */
    protected Logiclet onToken = null;

    protected HttpClientTool httpClientTool = null;

    protected static Pattern bearerPattern = Pattern.compile("^Bearer (.+)$");

    protected static Pattern basicPattern = Pattern.compile("^Basic (.+)$");

    protected static Pattern realmPattern = Pattern.compile("^\\[(\\w*)\\](.+)$");

    protected String contentType = "application/json;charset=utf-8";

    protected JsonFactory jsonFactory = Settings.getToolkit(JsonFactory.class);

    public void bizlog(String service,String clientIp,long time,long duration,String url,boolean error,String reason){
        BizLog.log(KeyGen.uuid(8,0,15),service,clientIp,error?CODE_ERR:CODE_OK,reason,time,duration,url);
    }

    @Override
    public void configure(Properties p) {
        cmdPrefix = PropertiesConstants.getString(p, "cmdPrefix",cmdPrefix);
        encoding = PropertiesConstants.getString(p, "encoding",encoding);
        service = PropertiesConstants.getString(p,"service",service);
        httpClientTool = Settings.getToolkit(HttpClientTool.class);

        realm = PropertiesConstants.getString(p,"realm",realm);
        Matcher matcher = realmPattern.matcher(realm);
        if (matcher.find()){
            this.realmMode = matcher.group(1);
            this.realm = matcher.group(2);
        }
    }

    @Override
    public void configure(Element e, Properties p) {
        Properties props = new XmlElementProperties(e,p);
        configure(props);

        Element elem = XmlTools.getFirstElementByPath(e, "on-auth");
        if (elem != null){
            onAuth = Script.create(elem, props);
        }

        elem = XmlTools.getFirstElementByPath(e, "on-token");
        if (elem != null){
            onToken = Script.create(elem, props);
        }
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        ServletConfigProperties props = new ServletConfigProperties(servletConfig);
        String master = PropertiesConstants.getString(props, "bearer.server.master", DEFAULT);
        String secondary = PropertiesConstants.getString(props, "bearer.server.secondary", DEFAULT);
        ResourceFactory rf = Settings.getResourceFactory();

        InputStream in = null;
        try {
            in = rf.load(master, secondary, null);
            Document doc = XmlTools.loadFromInputStream(in);
            if (doc != null){
                configure(doc.getDocumentElement(), props);
            }
        }catch (Exception ex){
            LOG.error("Can not init bearer handler with file : " + master);
        }finally{
            IOTools.close(in);
        }
    }

    @Override
    public void doService(HttpServletRequest httpReq, HttpServletResponse httpResp, String method) throws ServletException, IOException {
        try {
            String cmd = getCommand(httpReq.getContextPath(),httpReq.getRequestURI());
            if (StringUtils.isNotEmpty(cmd)){
                if (cmd.startsWith("/token")){
                    doToken(onToken,httpReq,httpResp);
                    return;
                }
            }
            doAuth(onAuth,httpReq,httpResp);
        }catch (BaseException ex){
            httpClientTool.sendError(httpResp,E404,String.format("%s:%s",ex.getCode(),ex.getMessage()));
        }
    }

    protected void doAuth(Logiclet handler,HttpServletRequest httpReq, HttpServletResponse httpResp) {
        if (handler == null){
            throw new BaseException("core.e1000","Handler on-auth is not defined");
        }

        String token = getBearerToken(httpReq,TOKEN_NULL);
        Context ctx = new HttpContext(httpReq,httpResp,encoding);
        LogicletContext logicletContext = new Context.ServantLogicletContext(ctx);
        long start = System.nanoTime();
        String clientIp = httpClientTool.getClientIp(httpReq);
        try {
            logicletContext.SetValue("$service", "/bearer/auth");
            logicletContext.SetValue("$clientIp",clientIp);
            logicletContext.SetValue("$token",token);

            logicletContext.SetValue("service",service);

            XsObject doc = new JsonObject("root",new HashMap<String,Object>());
            handler.execute(doc,doc, logicletContext, null);

            int status = PropertiesConstants.getInt(logicletContext,"$status",E200);
            String provider = PropertiesConstants.getString(logicletContext,"service",service);
            httpClientTool.setStatus(httpResp,status);
            if (status != E200){
                httpClientTool.setResponseHeader(httpResp,HEAD_WWW_AUTHENTICATE,
                        String.format(BEARER,getRealm(httpReq), provider));
            }

            OutputStream out = null;
            try {
                String data = jsonFactory.toJsonString(doc.getContent());
                ctx.setResponseContentType(contentType);
                out = ctx.getOutputStream();
                byte [] bytes = data.getBytes(ctx.getEncoding());
                ctx.setResponseContentLength(bytes.length);
                Context.writeToOutpuStream(out, bytes);
                out.flush();
            } catch (Exception ex) {
                LOG.error("Error when writing data to outputstream",ex);
            }finally {
                IOTools.close(out);
            }
        }finally{
            bizlog("/bearer/auth",clientIp,System.currentTimeMillis(),
                    System.nanoTime() - start,httpReq.getRequestURL().toString(),false,"");
        }
    }

    protected String getRealm(HttpServletRequest httpReq) {
        if (realmMode.equalsIgnoreCase("local")){
            return httpClientTool.getContextBase(httpReq) + realm;
        }else {
            return realm;
        }
    }

    protected String getBearerToken(HttpServletRequest httpReq, String dft) {
        String header = httpClientTool.getRequestHeader(httpReq,HEAD_AUTHORIZATION);
        if (StringUtils.isEmpty(header)){
            return dft;
        }
        Matcher matcher = bearerPattern.matcher(header);
        if (matcher.find()){
            return matcher.group(1);
        }else{
            return dft;
        }
    }

    protected String getBasicAuth(HttpServletRequest httpReq, String dft) {
        String header = httpClientTool.getRequestHeader(httpReq,HEAD_AUTHORIZATION);
        if (StringUtils.isEmpty(header)){
            return dft;
        }
        Matcher matcher = basicPattern.matcher(header);
        if (matcher.find()){
            return matcher.group(1);
        }else{
            return dft;
        }
    }

    protected void doToken(Logiclet handler,HttpServletRequest httpReq, HttpServletResponse httpResp) {
        if (handler == null){
            throw new BaseException("core.e1000","Handler on-token is not defined");
        }

        String basicAuth = getBasicAuth(httpReq,"0");
        Context ctx = new HttpContext(httpReq,httpResp,encoding);
        LogicletContext logicletContext = new Context.ServantLogicletContext(ctx);
        long start = System.nanoTime();
        String clientIp = httpClientTool.getClientIp(httpReq);
        try {
            logicletContext.SetValue("$service", "/bearer/token");
            logicletContext.SetValue("$clientIp",clientIp);
            logicletContext.SetValue("$auth",basicAuth);

            XsObject doc = new JsonObject("root",new HashMap<String,Object>());
            handler.execute(doc,doc, logicletContext, null);
            int status = PropertiesConstants.getInt(logicletContext,"$status",E200);
            httpClientTool.setStatus(httpResp,status);
            OutputStream out = null;
            try {
                String data = jsonFactory.toJsonString(doc.getContent());
                ctx.setResponseContentType(contentType);
                out = ctx.getOutputStream();
                byte [] bytes = data.getBytes(ctx.getEncoding());
                ctx.setResponseContentLength(bytes.length);
                Context.writeToOutpuStream(out, bytes);
                out.flush();
            } catch (Exception ex) {
                LOG.error("Error when writing data to outputstream",ex);
            }finally {
                IOTools.close(out);
            }
        }finally{
            bizlog("/bearer/token",clientIp,System.currentTimeMillis(),
                    System.nanoTime() - start,httpReq.getRequestURL().toString(),false,"");
        }
    }

    @Override
    public void destroy() {

    }

    protected String getCommand(String contextPath,String uri){
        String prefix = contextPath + this.cmdPrefix;
        if (uri.startsWith(prefix)){
            return uri.substring(prefix.length());
        }else{
            return "";
        }
    }

    protected String getParameter(HttpServletRequest request,String id,String dftValue){
        String value = request.getParameter(id);
        return StringUtils.isEmpty(value)?dftValue:value;
    }
}
